iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0

在 Rust 中,遮蔽(shadowing)是一種允許重複使用相同變數名稱的特性。

行為

遮蔽會在變數作用域內逐層生效,即在某一層的變數遮蔽了外層或之前定義的同名變數,當該層作用域結束後,外層的變數會重新生效。

main() {
    let x = 5;
    println!("{}", x); // 5
    
    let x = x + 1; // 遮蔽前一個 x
    println!("{}", x); // 6
    
    let x = x * 2; // 再次遮蔽
    println!("{}", x); // 12
}

特別的是,再宣告一個相同名稱的變數,並不是原本變數的值被更新了,而是創建了一個新的變數,只是原本的變數被遮蔽了所以編譯器讀取到的會是新的變數,佔據變數名稱的使用權,直到它自己也被遮蔽或是離開作用域:

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("x 在內部範圍的數值為:{x}");
    } // 內層定義 x 離開作用域,外部 x 重新生效

    println!("x 的數值為:{x}");
}
$ cargo run
x 在內部範圍的數值為:12
x 的數值為:6

這種做法有幾個好處:

  • 保持不可變性:每個 x 本身都是不可變的,符合 Rust 的安全理念。
  • 提高可讀性:我們可以重複使用有意義的變數名,而不是創建 x1、x2、x3 這樣的名稱。
  • 轉換型別:我們可以在同一個名稱下轉換變數的型別:
fn main() {
    let start: i32 = 1;
    println!("{}", start); // 1
    let start: bool = true; // 這行之後 1 被遮蔽了
    println!("{}", start); // true
}

這樣的設計和可變性(mutability)之間形成有一種有趣的關係,讓我們可以在不違反 Rust 安全原則的情況下,靈活地改變變數的值和型別。

安全性來說,新的變數一樣預設不可變,不受原本變數影響,所以如果直接去更新值還是一樣會報錯,驗證看看:

fn main() {
    let mut x = 5;
    x = x + 1;

    {
        let x = x * 2;
        x = x + 1;
        println!("x 在內部範圍的數值為:{x}");
    }

    println!("x 的數值為:{x}");
}
error[E0384]: cannot assign twice to immutable variable `x`
  --> src/main.rs:23:9
   |
22 |         let x = x * 2;
   |             -
   |             |
   |             first assignment to `x`
   |             help: consider making this binding mutable: `mut x`
23 |         x = x + 1;
   |         ^^^^^^^^^ cannot assign twice to immutable variable

比較一下一開始的例子,如果我們不使用遮蔽而使用可變變數的情況:

fn main() {
    let mut x = 5;
    println!("{}", x); // 5
    
    x = x + 1; // 直接修改 x 的值
    println!("{}", x); // 6
    
    x = x * 2; // 再次修改 x 的值
    println!("{}", x); // 12
}

這個版本看起來更簡潔,但有幾個潛在的問題:

  • x 在整個作用域內都是可變的,這增加了出錯的風險。
  • 無法改變 x 的型別。

使用情境

  1. 複雜數據轉換:當需要對數據進行多次轉換,但是不想宣告新變數的時候。這樣可以保持變數名稱的一致性,而不需要引入新的名稱,例如金額計算的時候。

    fn main() {
        let service_charge = true; // 要不要收服務費
        let price = 100.0;  // 基本價格
        let price = price * 1.08; // 加上 8% 的稅
        let price = price + 5.0; // 加上 5 元的點餐費
    
        let price = if service_charge {
            price * 1.1 // 如果需要加收服務費
        } else {
            price // 如果不需要加收服務費
        };
    
        println!("The final price is: ${:.0}", price);
    }
    
  2. 型別轉換:有時候最初拿到的型別只是暫時的,和我們實際要用的型別不同的時候,就不用變數名稱裡要再加上型別區分。

    use std::io;
    
    fn main() {
        println!("Please type a integer:");
    
        let mut guess = String::new();
    
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
    
        let guess: i32 = guess.trim().parse()
            .expect("Input is not a integer");
        // 其他操作、數字比較等等
    }
    

和其他程式語言比較

不只是 Rust ,很多其他程式語言也有 Shadowing 的概念

和 JavaScript 比較一下:

let x = 5;
{
    let x = 10;
    console.log(x); // 10, 內部作用域的 x
}
console.log(x); // 5, 外部作用域的 x

這個情況看起來差異不大,但其他情況就有差了。
JavaScript 要宣告一個已經被宣告的變數,要在不同作用域
如果在同一個作用域這樣寫就會報錯:

let x = 5;
let x = 10;
console.log(x);

那再來我們會改成這樣寫:

let x = 5;
x = 10;
console.log(x); // 10,

這樣可以的確正確執行,但其實就是 Rust 用可變變數的寫法,缺點也是一樣的,這個變數不具備不可變的特性,後面操作可以很輕易地改變原本的數值。當然 JavaScript 倒不會被型別限制住就是。

那 C++ 的情況呢?

#include <iostream>

int main() {
    int x = 5; // 外部作用域中的 x

    if (true) {
        int x = 10; // 內部作用域中的 x,遮蔽了外部的 x
        std::cout << "Inner x: " << x << std::endl; // 這裡輸出的是內部的 x,值為 10
    }

    std::cout << "Outer x: " << x << std::endl; // 這裡輸出的是外部的 x,值仍為 5

    return 0;
}

比較一下 Rust 的版本:

fn main() {
    let x = 5; // 外部作用域中的 x

    if true {
        let x = 10; // 內部作用域中的 x,遮蔽了外部的 x
        println!("Inner x: {}", x); // 這裡輸出的是內部的 x,值為 10
    }

    println!("Outer x: {}", x); // 這裡輸出的是外部的 x,值仍為 5
}
 

乍看還是和 Rust 沒有太大差別,但有一個差別是,和 Rust 是顯式地引入遮蔽(需要開發者明確地用 let 宣告,換句話說,開發者主動的),而 C++ 不是,沒有仔細看很容易沒發現現在看到的變數是在哪一層的作用域中,容易造成非預期影響。Rust 的這種顯式遮蔽設計不僅提高了代碼的可讀性,還能在編譯時就捕獲潛在的錯誤,體現了Rust 對安全性的重視。

結語

Rust 的遮蔽機制讓我們能夠在保持變數不可變性的同時,實現一種受控的可變性。這種設計體現了 Rust 的哲學:透過語言設計來提供安全保證,同時保持足夠的彈性。兼顧彈性的設計都是從很多細節的地方累積起來的,Rust 真的是一個很細心的語言。


上一篇
Day8 - 變數與常數
下一篇
Day10 - 流程控制
系列文
螃蟹幼幼班:Rust 入門指南25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言